/**
 * Licensed to Jasig under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Jasig licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a
 * copy of the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package  org.jasig.portal.security.provider;

import java.util.Properties;

import javax.naming.AuthenticationException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

import org.jasig.portal.ldap.LdapServices;
import org.jasig.portal.ldap.ILdapServer;
import org.jasig.portal.security.IConfigurableSecurityContext;
import org.jasig.portal.security.PortalSecurityException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * <p>This is an implementation of a SecurityContext that checks a user's
 * credentials against an LDAP directory.  It expects to be able to bind
 * to the LDAP directory as the user so that it can authenticate the
 * user.</p>
 * <p>
 * By implementing the {@link org.jasig.portal.security.IConfigurableSecurityContext}
 * interface this context may have properties set on it. The one property
 * the <code>SimpleLdapSecurityContext</code> looks for is defined by
 * the String {@link #LDAP_PROPERTIES_CONNECTION_NAME} "connection".
 * This property allows a specific, named, LDAP connection to be used by
 * the context. If no "connection" property is specified the default
 * LDAP connection returned by {@link org.jasig.portal.ldap.LdapServices} is
 * used. 
 * </p>
 *
 * @author Russell Tokuyama (University of Hawaii)
 * @version $Revision$
 */
public class SimpleLdapSecurityContext extends ChainingSecurityContext
    implements IConfigurableSecurityContext {
    
    private static final Log log = LogFactory.getLog(SimpleLdapSecurityContext.class);
    
  // Attributes that we're interested in.
  public static final int ATTR_UID = 0;
  public static final int ATTR_FIRSTNAME = ATTR_UID + 1;
  public static final int ATTR_LASTNAME = ATTR_FIRSTNAME + 1;
  private final int SIMPLE_LDAP_SECURITYAUTHTYPE = 0xFF04;
  private static final String[] attributes =  {
    "uid",      // user ID
    "givenName",                // first name
    "sn"        // last name
  };
  
  public static final String LDAP_PROPERTIES_CONNECTION_NAME = "connection";
  private Properties ctxProperties;

  SimpleLdapSecurityContext() {
    super();
    ctxProperties = new Properties();
  }
  
  /**
   * Sets the properties to use for this security context.
   * 
   * @see org.jasig.portal.security.IConfigurableSecurityContext#setProperties(java.util.Properties)
   */
  public void setProperties(Properties props)
  {
      ctxProperties = props;
  }  

  /**
   * Returns the type of authentication this class provides.
   * @return authorization type
   */
  public int getAuthType () {
    /*
     * What is this for?  No one would know what to do with the
     * value returned.  Subclasses might know but our getAuthType()
     * doesn't return anything easily useful.
     */
    return  this.SIMPLE_LDAP_SECURITYAUTHTYPE;
  }

  /**
   * Authenticates the user.
   */
  public synchronized void authenticate () throws PortalSecurityException {
    this.isauth = false;
    ILdapServer ldapConn;
    
    String propFile = ctxProperties.getProperty(LDAP_PROPERTIES_CONNECTION_NAME);
    if(propFile != null && propFile.length() > 0)
        ldapConn = LdapServices.getLdapServer(propFile);
    else
        ldapConn = LdapServices.getDefaultLdapServer();    
    
    String creds = new String(this.myOpaqueCredentials.credentialstring);
    if (this.myPrincipal.UID != null && !this.myPrincipal.UID.trim().equals("") && this.myOpaqueCredentials.credentialstring
        != null && !creds.trim().equals("")) {
      DirContext conn = null;
      NamingEnumeration results = null;
      StringBuffer user = new StringBuffer("(");
      String first_name = null;
      String last_name = null;
      
      user.append(ldapConn.getUidAttribute()).append("=");
      user.append(this.myPrincipal.UID).append(")");
      if (log.isDebugEnabled())
          log.debug(
                     "SimpleLdapSecurityContext: Looking for " +
                     user.toString());
      
      try {
          conn = ldapConn.getConnection();
          
          // set up search controls
          SearchControls searchCtls = new SearchControls();
          searchCtls.setReturningAttributes(attributes);
          searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
          
          // do lookup
          if (conn != null) {
              try {
                results = conn.search(ldapConn.getBaseDN(), user.toString(), searchCtls);
                if (results != null) {
                  if (!results.hasMore())
                    log.error(
                                   "SimpleLdapSecurityContext: user not found , " +
                                   this.myPrincipal.UID);
                  while (results != null && results.hasMore()) {
                    SearchResult entry = (SearchResult)results.next();
                    StringBuffer dnBuffer = new StringBuffer();
                    dnBuffer.append(entry.getName()).append(", ");
                    dnBuffer.append(ldapConn.getBaseDN());
                    Attributes attrs = entry.getAttributes();
                    first_name = getAttributeValue(attrs, ATTR_FIRSTNAME);
                    last_name = getAttributeValue(attrs, ATTR_LASTNAME);
                    // re-bind as user
                    conn.removeFromEnvironment(javax.naming.Context.SECURITY_PRINCIPAL);
                    conn.removeFromEnvironment(javax.naming.Context.SECURITY_CREDENTIALS);
                    conn.addToEnvironment(javax.naming.Context.SECURITY_PRINCIPAL, dnBuffer.toString());
                    conn.addToEnvironment(javax.naming.Context.SECURITY_CREDENTIALS, this.myOpaqueCredentials.credentialstring);
                    searchCtls = new SearchControls();
                    searchCtls.setReturningAttributes(new String[0]);
                    searchCtls.setSearchScope(SearchControls.OBJECT_SCOPE);
    
                    String attrSearch = "(" + ldapConn.getUidAttribute() + "=*)";
                    log.debug(
                                   "SimpleLdapSecurityContext: Looking in " +
                                   dnBuffer.toString() + " for " + attrSearch);
                    conn.search(dnBuffer.toString(), attrSearch, searchCtls);
    
                    this.isauth = true;
                    this.myPrincipal.FullName = first_name + " " + last_name;
                    log.debug(
                                   "SimpleLdapSecurityContext: User " +
                                   this.myPrincipal.UID + " (" +
                                   this.myPrincipal.FullName + ") is authenticated");
    
                    // Since LDAP is case-insensitive with respect to uid, force
                    // user name to lower case for use by the portal
                    this.myPrincipal.UID = this.myPrincipal.UID.toLowerCase();
                  } // while (results != null && results.hasMore())
                }
                else {
                  log.error(
                                 "SimpleLdapSecurityContext: No such user: " +
                                 this.myPrincipal.UID);
                }
              } catch (AuthenticationException ae) {
                log.info("SimpleLdapSecurityContext: Password invalid for user: " + this.myPrincipal.UID);
              } catch (Exception e) {
                log.error(
                               "SimpleLdapSecurityContext: LDAP Error with user: " +
                               this.myPrincipal.UID + "; ", e);
                throw new PortalSecurityException("SimpleLdapSecurityContext: LDAP Error" + e + " with user: " + this.myPrincipal.UID);
              } finally {
                ldapConn.releaseConnection(conn);
              }
          }
          else {
            log.error("LDAP Server Connection unavalable");
          }
      }
      catch (final NamingException ne) {
          log.error("Error geting connection to LDAP server.", ne);
      }
    }
    else {
      log.error( "Principal or OpaqueCredentials not initialized prior to authenticate");
    }
    // Ok...we are now ready to authenticate all of our subcontexts.
    super.authenticate();
    return;
  }

  /*--------------------- Helper methods ---------------------*/
  /**
   * <p>Return a single value of an attribute from possibly multiple values,
   * grossly ignoring anything else.  If there are no values, then
   * return an empty string.</p>
   *
   * @param attrs LDAP query results
   * @param attribute LDAP attribute we are interested in
   * @return a single value of the attribute
   */
  private String getAttributeValue (Attributes attrs, int attribute) throws NamingException {
    NamingEnumeration values = null;
    String aValue = "";
    if (!isAttribute(attribute))
      return  aValue;
    Attribute attrib = attrs.get(attributes[attribute]);
    if (attrib != null) {
      for (values = attrib.getAll(); values.hasMoreElements();) {
        aValue = (String)values.nextElement();
        break;                  // take only the first attribute value
      }
    }
    return  aValue;
  }

  /**
   * Is this a value attribute that's been requested?
   *
   * @param attribute in question
   */
  private boolean isAttribute (int attribute) {
    if (attribute < ATTR_UID || attribute > ATTR_LASTNAME) {
      return  false;
    }
    return  true;
  }
}



